/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Xml.XPath;
using System.ComponentModel.Design;
using System.Windows.Forms.Design;

namespace DaveSexton.DocProject.Sandcastle.BuildComponents.TypeConverters.BuiltIn
{
	internal sealed class TransformComponentTypeConverter : TypeConverter
	{
		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="TransformComponentTypeConverter" /> class.
		/// </summary>
		public TransformComponentTypeConverter()
		{
		}
		#endregion

		#region Methods
		private static IEnumerable<IXPathNavigable> GetTransforms(IXPathNavigable navigable)
		{
			if (navigable != null)
			{
				XPathNavigator navigator = navigable.CreateNavigator();

				foreach (XPathNavigator node in navigator.SelectChildren("transform", ""))
				{
					string file = node.GetAttribute("file", "");

					if (file != null && (file = file.Trim()).Length > 0)
						yield return node.CreateNavigator();
				}
			}
		}

		public override bool GetPropertiesSupported(ITypeDescriptorContext context)
		{
			return true;
		}

		public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
		{
			PropertyDescriptorCollection properties = new PropertyDescriptorCollection(null);

			int i = 0;
			foreach (IXPathNavigable transform in GetTransforms(value as IXPathNavigable))
				properties.Add(new TransformPropertyDescriptor("Transform" + i++, transform, context));

			return properties;
		}

		public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
		{
			if (destinationType == typeof(string))
			{
				List<IXPathNavigable> files = new List<IXPathNavigable>(GetTransforms(value as IXPathNavigable));

				return string.Format(System.Globalization.CultureInfo.CurrentCulture, Resources.Text.TransformComponentValue, files.Count);
			}
			else
				return base.ConvertTo(context, culture, value, destinationType);
		}
		#endregion

		#region Nested
		private sealed class TransformPropertyDescriptor : PropertyDescriptor
		{
			#region Public Properties
			public override Type ComponentType
			{
				get
				{
					return context.PropertyDescriptor.ComponentType;
				}
			}

			public override bool IsReadOnly
			{
				get
				{
					return false;
				}
			}

			public override Type PropertyType
			{
				get
				{
					return typeof(string);
				}
			}

			public override string DisplayName
			{
				get
				{
					return displayName;
				}
			}

			public override string Description
			{
				get
				{
					return string.Format(System.Globalization.CultureInfo.CurrentCulture,
						Resources.Text.TransformComponentTransformDescription, transform.GetAttribute("file", ""), Environment.NewLine, Arguments.Count);
				}
			}

			public ICollection<IXPathNavigable> Arguments
			{
				get
				{
					return new List<IXPathNavigable>(GetArguments(transform));
				}
			}
			#endregion

			#region Private / Protected
			private readonly XPathNavigator transform;
			private readonly string displayName, defaultFile;
			private readonly ITypeDescriptorContext context;
			#endregion

			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="TransformPropertyDescriptor" /> class.
			/// </summary>
			public TransformPropertyDescriptor(string name, IXPathNavigable transform, ITypeDescriptorContext context)
				: base(name, new Attribute[] { new NotifyParentPropertyAttribute(true), new TypeConverterAttribute(typeof(TransformArgumentTypeConverter)) })
			{
				this.transform = transform.CreateNavigator();
				this.context = context;

				defaultFile = this.transform.GetAttribute("file", "");
				displayName = CreateDisplayName(defaultFile);
			}
			#endregion

			#region Methods
			private static IEnumerable<IXPathNavigable> GetArguments(IXPathNavigable navigable)
			{
				if (navigable != null)
				{
					XPathNavigator navigator = navigable.CreateNavigator();

					foreach (XPathNavigator node in navigator.SelectChildren("argument", ""))
						yield return node.CreateNavigator();
				}
			}

			private static string CreateDisplayName(string file)
			{
				Uri uri;
				if (Uri.TryCreate(Environment.ExpandEnvironmentVariables(file), UriKind.RelativeOrAbsolute, out uri))
					return System.IO.Path.GetFileName((uri.IsAbsoluteUri) ? uri.LocalPath : System.Web.HttpUtility.UrlDecode(uri.ToString()));
				else
					return file;
			}

			public override bool ShouldSerializeValue(object component)
			{
				return !string.Equals(transform.GetAttribute("file", ""), defaultFile, StringComparison.OrdinalIgnoreCase);
			}

			public override bool CanResetValue(object component)
			{
				return true;
			}

			public override void ResetValue(object component)
			{
				XPathNavigator navigator = transform.CreateNavigator();

				if (navigator.MoveToAttribute("file", ""))
					navigator.SetValue(defaultFile);
				else
					navigator.CreateAttribute(null, "file", null, defaultFile);
			}

			public override object GetValue(object component)
			{
				return transform.GetAttribute("file", "");
			}

			public override void SetValue(object component, object value)
			{
				XPathNavigator navigator = transform.CreateNavigator();
				string str = (string) value;

				if (navigator.MoveToAttribute("file", ""))
					navigator.SetValue(str);
				else
					navigator.CreateAttribute(null, "file", null, str);
			}
			#endregion
		}

		private sealed class TransformArgumentTypeConverter : TypeConverter
		{
			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="TransformArgumentTypeConverter" /> class.
			/// </summary>
			public TransformArgumentTypeConverter()
			{
			}
			#endregion

			#region Methods
			public override bool GetPropertiesSupported(ITypeDescriptorContext context)
			{
				return true;
			}

			public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
			{
				PropertyDescriptorCollection properties = new PropertyDescriptorCollection(null);

				TransformPropertyDescriptor transform = (TransformPropertyDescriptor) context.PropertyDescriptor;

				int i = 0;
				foreach (IXPathNavigable argument in transform.Arguments)
					properties.Add(new ArgumentPropertyDescriptor("Argument" + i++, argument, context));

				return properties;
			}
			#endregion
		}

		private sealed class ArgumentPropertyDescriptor : PropertyDescriptor
		{
			#region Public Properties
			public override Type ComponentType
			{
				get
				{
					return context.PropertyDescriptor.ComponentType;
				}
			}

			public override bool IsReadOnly
			{
				get
				{
					return false;
				}
			}

			public override Type PropertyType
			{
				get
				{
					return typeof(string);
				}
			}

			public override string DisplayName
			{
				get
				{
					return displayName;
				}
			}

			public override string Description
			{
				get
				{
					return string.Format(System.Globalization.CultureInfo.CurrentCulture,
						Resources.Text.TransformComponentArgumentDescription, displayName, Environment.NewLine, argument.GetAttribute("value", ""));
				}
			}

			public IXPathNavigable Argument
			{
				get
				{
					return argument.CreateNavigator();
				}
			}
			#endregion

			#region Private / Protected
			private readonly XPathNavigator argument;
			private readonly string displayName, defaultValue, defaultInnerXml;
			private readonly ITypeDescriptorContext context;
			private bool ignoreValueChange;
			#endregion

			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="ArgumentPropertyDescriptor" /> class.
			/// </summary>
			public ArgumentPropertyDescriptor(string name, IXPathNavigable argument, ITypeDescriptorContext context)
				: base(name, new Attribute[] { new NotifyParentPropertyAttribute(true), new EditorAttribute(typeof(ArgumentEditor), typeof(System.Drawing.Design.UITypeEditor)) })
			{
				this.argument = argument.CreateNavigator();
				this.context = context;

				displayName = this.argument.GetAttribute("key", "");
				defaultValue = this.argument.GetAttribute("value", "");
				defaultInnerXml = this.argument.InnerXml;
			}
			#endregion

			#region Methods
			public override bool ShouldSerializeValue(object component)
			{
				return !string.Equals(argument.GetAttribute("value", ""), defaultValue, StringComparison.OrdinalIgnoreCase)
					|| !string.Equals(argument.InnerXml, defaultInnerXml, StringComparison.Ordinal);
			}

			public override bool CanResetValue(object component)
			{
				return true;
			}

			public override void ResetValue(object component)
			{
				XPathNavigator navigator = argument.CreateNavigator();

				navigator.InnerXml = defaultInnerXml;

				if (navigator.MoveToAttribute("value", ""))
					navigator.SetValue(defaultValue);
				else
					navigator.CreateAttribute(null, "value", null, defaultValue);
			}

			public override object GetValue(object component)
			{
				return argument.GetAttribute("value", "");
			}

			public override void SetValue(object component, object value)
			{
				if (ignoreValueChange)
				{
					ignoreValueChange = false;
					return;
				}

				XPathNavigator navigator = argument.CreateNavigator();
				string str = (string) value;

				if (navigator.MoveToAttribute("value", ""))
					navigator.SetValue(str);
				else
					navigator.CreateAttribute(null, "value", null, str);
			}

			public void SuspendUpdate()
			{
				ignoreValueChange = true;
			}
			#endregion
		}

		private sealed class ArgumentEditor : System.Drawing.Design.UITypeEditor
		{
			#region Private / Protected
			private readonly MultilineStringEditor editor = new MultilineStringEditor();
			#endregion

			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="ArgumentEditor" /> class.
			/// </summary>
			public ArgumentEditor()
			{
			}
			#endregion

			#region Methods
			public override System.Drawing.Design.UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
			{
				return System.Drawing.Design.UITypeEditorEditStyle.DropDown;
			}

			public override bool GetPaintValueSupported(ITypeDescriptorContext context)
			{
				return false;
			}

			public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
			{
				if (context == null)
					throw new ArgumentNullException("context");

				// NOTE: This editor doesn't actually edit the argument's "value", it edits the argument's inner XML.

				ArgumentPropertyDescriptor property = (ArgumentPropertyDescriptor) context.PropertyDescriptor;

				IXPathNavigable argument = property.Argument;
				XPathNavigator navigator = argument.CreateNavigator();
				string xml = navigator.InnerXml;

				System.Xml.XmlDocument document = new System.Xml.XmlDocument();
				System.Xml.XmlNode root = document.AppendChild(document.CreateElement("root"));

				// remove leading white space
				// (XmlDocument.PreserveWhitespace does not seem to preserve leading white space in a fragment, 
				// so if this isn't done then there may be leading white space displayed in the dialog but it will 
				// be removed when the changes are passed back though the XmlDocument for validation below)
				root.InnerXml = xml;
				xml = root.InnerXml;

				while (true)
				{
					try
					{
						object result = editor.EditValue(context, provider, xml);

						if (result == null)
							// canceled
							break;
						else
						{
							xml = (string) result;

							document.PreserveWhitespace = true;

							root.InnerXml = xml;

							navigator.InnerXml = root.InnerXml;
							break;
						}
					}
					catch (System.Xml.XmlException ex)
					{
						if (DocProjectEnvironment.ShowErrorDialog(ex, System.Windows.Forms.MessageBoxButtons.RetryCancel, System.Windows.Forms.MessageBoxDefaultButton.Button1,
							"The specified XML is invalid." + Environment.NewLine + Environment.NewLine + 
							"Do you want to retry or cancel the edit and lose your changes?") == System.Windows.Forms.DialogResult.Cancel)
							break;
					}
				}

				// HACK: let the descriptor know that the value we're returning has been modified only to let the property grid
				// know that changes were made (since the inner XML was edited, not the actual value).  When the next assignment 
				// occurs the value must be ignored.  Subsequent updates should resume as normal.
				property.SuspendUpdate();

				return ((string) value) + ".";
			}
			#endregion
		}
		#endregion
	}
}
